package com.kili.cloud.shorturl.service.impl;

import com.kili.cloud.common.exception.ExceptionUtils;
import com.kili.cloud.common.util.RedisLock;
import com.kili.cloud.shorturl.util.TinyUrl;
import com.kili.cloud.shorturl.enumerate.IsDeletedEnum;
import com.kili.cloud.shorturl.enumerate.ShortUrlTypeEnum;
import com.kili.cloud.shorturl.service.AbstractShortUrl;
import com.kili.cloud.shorturl.service.CustomerService;
import com.kili.cloud.shorturl.service.UrlMapInfoService;
import com.kili.cloud.shorturl.service.model.UrlMapInfoModel;
import com.kili.cloud.shorturl.vo.ShortUrlParam;
import com.kili.common.Exception.ExceptionType;
import com.kili.tools.redis.RedisUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;

/**
 * @description: 正常模式下（验证用户信息）的短链接处理对象
 * @author: tom.liu
 * @date: 2019/6/16 23:29
 */
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class NormalShortUrl extends AbstractShortUrl {
    private static Logger logger = LoggerFactory.getLogger(NormalShortUrl.class);

    /**
     * redis中存储用户调用录入接口次数的缓存前缀
     */
    private static  final String  USER_INPUT_COUNT = "short_url:user_input_count:";

    /**
     * redis中存储用户调用访问接口次数的缓存前缀
     */
    private static  final String  USER_VISIT_COUNT = "short_url:user_visit_count:";

    /**
     * redis中存储用户调用访问数据的缓存前缀
     */
    private static  final String  USER_VISIT_DATA = "short_url:user_visit_data:";

    @Autowired
    CustomerService customerService;

    @Autowired
    RedisUtils redisUtils;

    @Autowired
    UrlMapInfoService urlMapInfoService;

    @Autowired
    RedisTemplate<String, Object> redisTemplate;



    @Override
    public void validateInput(ShortUrlParam param) {
        logger.debug("NormalShortUrl.validateInput-验证方法开始");
        //验证用户信息
        validateCustomer(param.getUserName(), param.getPassword());
        //校验输入的参数信息
        validateParam(param);
        //校验是否超过每分钟的最大存入次数
        validateMaxInputCount();
        logger.debug("NormalShortUrl.validateInput-验证方法：通过");
    }

    @Override
    public String inputBussiness(ShortUrlParam param) {

        //判断是否已经保存过，是的话直接返回压缩后的url
        String url = inputOldBussiness(param);
        //如果未保存，则执行新增相关业务操作操作
        if(StringUtils.isEmpty(url)) {
            logger.debug("该url已经未保存过,进行相关处理");
            url = inputNewBussiness(param);
        }else{
            logger.debug("该url已经保存过，直接返回，url={}", url);
        }
        return url;
    }

    @Override
    public void validateVisit(String uri, String ip) {
        logger.debug("NormalShortUrl.validateVisit-验证方法开始");
        //校验ip是否为空
        validateIp(ip);
        //校验访问次数是否过于频繁，超过最大值
        validateMaxVisitCount(ip);
        logger.debug("NormalShortUrl.validateVisit-验证方法：通过");
    }

    @Override
    public String visitBussiness(String uri) {
        //判断是否在redis已经保存过，是的话直接返回压缩后的url
        String originalUrl = visitOldBussiness(uri);
        //如果未保存，则执行新增相关业务操作操作
        if(StringUtils.isBlank(originalUrl)) {
            logger.debug("该短链接url未保存到redis,进行相关处理");
            originalUrl = visitNewBussiness(uri);
        }else{
            logger.debug("该短链接url已经保存到redis，直接返回，uri={},originalUrl={}", uri, originalUrl);
        }
        return originalUrl;
    }

    /**
     * 功能描述: 验证用户信息
     * @param: String 用户名
     * @param: String 密码
     * @return:
     * @author: tom.liu
     * @date: 2019/6/17 11:38
     */
    private void validateCustomer(String userName, String password){
        //验证用户信息
        long custId = customerService.validateUser(userName, password);
        this.getContext().setCustomerId(custId);
        logger.debug("validateCustomer-验证用户信息通过");
    }
    /**
     * 功能描述: 验证输入的参数
     * @param: ShortUrlParam 长链接参数
     * @return:
     * @author: tom.liu
     * @date: 2019/6/17 11:35
     */
    private void validateParam(ShortUrlParam param){
        //判断输入的url是否为空
        if(StringUtils.isBlank(param.getOriginalUrl())){
            logger.info("输入的url为空");
            throw  ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "the url entered  is empty" );
        }
        //判断短链接类型是否合法
        if(ShortUrlTypeEnum.getByCode(param.getType()) == null){
            logger.info("输入的短链接类型不合法");
            throw ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "the url type entered is not legal" );
        }
        //次数型短链接需要输入有效的最大访问次数
        boolean effectiveCountFlag = param.getType().equals(ShortUrlTypeEnum.FREQUENCY.getCode()) && (param.getEffectCount() == null || param.getEffectCount() <= 0);
        if(effectiveCountFlag){
            logger.info("次数型短链接需要输入最大有效次数不合法");
            throw ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "the effective count is not legal" );
        }

        //时间型短链接需要输入起始和终止有效时间
        boolean outTimeFlag = param.getType().equals(ShortUrlTypeEnum.TIME_FRAME.getCode()) &&
                (param.getEffectTimeBegin() == null || param.getEffectTimeEnd() == null);
        if(outTimeFlag){
            logger.info("时间型短链接需要输入起始和终止有效时间");
            throw ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "time frame url need to input time frame" );
        }
    }

    /**
     * 功能描述: 校验用户一分钟内是否超过最大访问次数-用于限流
     * @param:
     * @return:
     * @author: tom.liu
     * @date: 2019/6/17 14:26
     */
    private void validateMaxInputCount(){
        //如果已经存在，且没有超过单位时间内的最大访问次数，则直接加1，否则抛出异常
        if(redisUtils.hasKey(USER_INPUT_COUNT + this.getContext().getCustomerId())){
            Integer count = redisUtils.get(USER_INPUT_COUNT + this.getContext().getCustomerId(), Integer.class);
            if(count != null && count.intValue() < this.getContext().getShortUrlMaxInputCount()){
                redisUtils.increment(USER_INPUT_COUNT + this.getContext().getCustomerId(), 1);
            }else{
                logger.info("单位时间内超过的最大存入次数：customerId={}, count={}", this.getContext().getCustomerId(), this.getContext().getShortUrlMaxInputCount());
                throw ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "the number of entries is too frequent" );
            }
        }else {
            //不存在则存入，并设置过期时间
            redisTemplate.opsForValue().setIfAbsent(USER_INPUT_COUNT + getContext().getCustomerId(), 1);
            redisUtils.expire(USER_INPUT_COUNT + getContext().getCustomerId(), 60);
        }
    }

    /**
     * 功能描述: 判断输入的url是否保存过，保存过则直接返回压缩后的短链接
     * @param: ShortUrlParam 长链接参数
     * @return: String 压缩后的短链接
     * @author: tom.liu
     * @date: 2019/6/17 13:43
     */
    private String inputOldBussiness(ShortUrlParam param){
        String url = "";
        //计算传入的url的hashCode值
        long hashCode = getContext().getHashCode();
        //验证该客户输入的url是否已经保存过
        UrlMapInfoModel urlMapInfoReq = new UrlMapInfoModel();
        urlMapInfoReq.setCustomerId(getContext().getCustomerId());
        urlMapInfoReq.setHashCode(hashCode);
        urlMapInfoReq.setIsDeleted(IsDeletedEnum.NO.getCode());
        List<UrlMapInfoModel> urlMapInfoModelList = urlMapInfoService.selectBySelective(urlMapInfoReq);
        //绝大部分请求是新请求，即查询出来的urlMapInfoModelList将为空，这样通过hashcode减少比对开销
        if(CollectionUtils.isEmpty(urlMapInfoModelList)) {
            return url;
        }

        //如果存在记录，则再次通过原url来区分，因为hashcode的求值机制导致hashcode可能重复
        Date now = new Date();
        for (UrlMapInfoModel urlMapInfoModel : urlMapInfoModelList) {
            if(StringUtils.equals(urlMapInfoModel.getOriginalUrl(), param.getOriginalUrl())){
                //判断是否过期,过期了不认为是重复，需要插入新的记录
                 boolean sameFlag = urlMapInfoModel.getType().equals(param.getType()) && (urlMapInfoModel.getType().equals(ShortUrlTypeEnum.PERMANENT.getCode())
                        || (urlMapInfoModel.getType().equals(ShortUrlTypeEnum.FREQUENCY.getCode()) && urlMapInfoModel.getEffectCount().equals(param.getEffectCount()))
                        || (urlMapInfoModel.getType().equals(ShortUrlTypeEnum.TIME_FRAME.getCode()) && urlMapInfoModel.getEffectTimeBegin().equals(param.getEffectTimeBegin()) && urlMapInfoModel.getEffectTimeEnd().equals(param.getEffectTimeEnd())));
                //相同的录入请求，则返回已经保存的短链接，否则失效数据库的记录，重新插入一条
                if(sameFlag){
                    url = urlMapInfoModel.getShortUrlDomain() + urlMapInfoModel.getShortUrlUri();
                    break;
                }else{
                    //更新is_deleted标志位，后面插入一条新的记录
                    urlMapInfoModel.setIsDeleted(IsDeletedEnum.YES.getCode());
                    urlMapInfoModel.setUpdateTime(now);
                    urlMapInfoService.updateByPrimaryKeySelective(urlMapInfoModel);
                }
            }
        }

        return url;
    }

    /**
     * 功能描述: 访问一个已经在缓存中的短链接,直接返回（次数型不存redis）
     * @param:
     * @return:
     * @author: tom.liu
     * @date: 2019/6/17 16:57
     */
    private String visitOldBussiness(String uri){
        //如果redis已经缓存了该短链接的数据,直接返回
        String originalUrl = "";
        if(redisUtils.hasKey(USER_VISIT_DATA + uri)){
            UrlMapInfoModel urlMapInfoModel = redisUtils.get(USER_VISIT_DATA + uri, UrlMapInfoModel.class);
            //时间型短链接判断是否已经过期
            Date now = new Date();
            if(urlMapInfoModel.getType().equals(ShortUrlTypeEnum.TIME_FRAME.getCode())) {
                //已超时
                if (urlMapInfoModel.getEffectTimeBegin().after(now) || urlMapInfoModel.getEffectTimeEnd().before(now)) {
                    logger.info("该短链接数据已过期，uri={}", uri);
                    throw ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "data does has expired" );
                }
            }
            originalUrl = urlMapInfoModel.getOriginalUrl();

            //如果存在数据，则访问次数加1
            urlMapInfoService.updateVisitCount(urlMapInfoModel.getId());
        }
        return originalUrl;
    }

    /**
     * 功能描述: 需要新增记录情况下的短链接处理逻辑(存在多线程的风险，但影响不大，后期解决)
     * @param: ShortUrlParam 长链接参数
     * @return: String 压缩后的短链接
     * @author: tom.liu
     * @date: 2019/6/17 11:32
     */
    private String inputNewBussiness(ShortUrlParam param){
        //存入长链接信息，得到id
        UrlMapInfoModel urlMapInfoModel = new UrlMapInfoModel();
        urlMapInfoModel.setOriginalUrl(param.getOriginalUrl());
        urlMapInfoModel.setShortUrlDomain(getContext().getShortUrlDomain());
        urlMapInfoModel.setHashCode(getContext().getHashCode());
        urlMapInfoModel.setType(param.getType());
        urlMapInfoModel.setCustomerId(getContext().getCustomerId());
        urlMapInfoModel.setEffectCount(param.getEffectCount());
        urlMapInfoModel.setEffectTimeBegin(param.getEffectTimeBegin());
        urlMapInfoModel.setEffectTimeEnd(param.getEffectTimeEnd());
        urlMapInfoModel.setIsDeleted(IsDeletedEnum.NO.getCode());
        long id = urlMapInfoService.insertSelective(urlMapInfoModel);
        urlMapInfoModel.setId(id);
        //将id转为32进制
        String uri = TinyUrl.numberConvertToDecimal(id, 32);
        urlMapInfoModel.setShortUrlUri(uri);
        //更新uri字段
        urlMapInfoService.updateByPrimaryKeySelective(urlMapInfoModel);

        return urlMapInfoModel.getShortUrlDomain() + urlMapInfoModel.getShortUrlUri();
    }
    
    /**
     * 功能描述: 校验ip是否为空
     * @param: String
     * @return: 
     * @author: tom.liu
     * @date: 2019/6/17 16:05
     */
    private void validateIp(String ip){
        if(StringUtils.isBlank(ip)){
            logger.info("输入的ip为空");
            throw ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "ip is null" );
        }
    }

    /**
     * 功能描述: 验证当前ip是否调用过于频繁（超过设定的最大值）
     * @param: String 压缩后的url
     * @param: String 访问ip
     * @return: 
     * @author: tom.liu
     * @date: 2019/6/17 16:08
     */
    private void validateMaxVisitCount(String ip) {
        //如果已经存在，且没有超过单位时间内的最大访问次数，则直接加1，否则抛出异常
        if (redisUtils.hasKey(USER_VISIT_COUNT + ip)) {
            Integer count = redisUtils.get(USER_VISIT_COUNT + ip, Integer.class);
            if (count != null && count.intValue() < getContext().getShortUrlMaxVisitCount()) {
                redisUtils.increment(USER_VISIT_COUNT + ip, 1);
            } else {
                logger.info("单位时间内该ip访问超过的最大次数：ip={},count={}", ip, getContext().getShortUrlMaxVisitCount());
                throw ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "the number of visits is too frequent" );
            }
        } else {
            //不存在则存入，并设置过期时间
            redisTemplate.opsForValue().setIfAbsent(USER_VISIT_COUNT + ip, 1);
            redisUtils.expire(USER_VISIT_COUNT + ip, 60);
        }
    }

    /**
     * 功能描述: 不再缓存中的数据访问方法
     * @param: String 资源定位符
     * @return: String 原链接
     * @author: tom.liu
     * @date: 2019/6/17 17:43
     */
    private String visitNewBussiness(String uri){
        //从db中获取数据(sql已排除过期数据)
        UrlMapInfoModel urlMapInfoModel = urlMapInfoService.selectOriginalUrlByUri(uri);
        //如果数据不存在则报异常
        if(urlMapInfoModel == null){
            logger.info("该短链接数据不存在或者已过期，uri={}", uri);
            throw ExceptionUtils.getBussinessException(ExceptionType.BIZ_VERIFY_ERROR, "500", "data does not exist or has expired" );
        }else{

            //如果存在数据且类型为次数型，则访问次数加1
            urlMapInfoService.updateVisitCount(urlMapInfoModel.getId());

            //非次数型请求放入redis保存5分钟--次数型请求放入redis后会带来更新redis缓存的操作，且省不了数据库操作
            if(!urlMapInfoModel.getType().equals(ShortUrlTypeEnum.FREQUENCY.getCode())) {
                redisUtils.set(USER_VISIT_DATA + uri, urlMapInfoModel, 60 * 5);
            }
        }

        return urlMapInfoModel.getOriginalUrl();
    }
}
